/*
 * Copyright (C) 2007-2012 by XDEV Software, All Rights Reserved.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License version 3.0 as published by the Free Software Foundation.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 */
 
package xdev.io.csv;


import java.io.IOException;
import java.util.ArrayList;
import java.util.List;


/**
 * A very simple CSV parser. This just implements splitting a single line into
 * fields.
 * 
 * @author Glen Smith
 * @author Rainer Pruy
 * 
 */
public class CSVParser
{
	private final char			separator;
	
	private final char			quotechar;
	
	private final char			escape;
	
	private final boolean		strictQuotes;
	
	private String				pending;
	private boolean				inField								= false;
	
	private final boolean		ignoreLeadingWhiteSpace;
	
	/** The default separator to use if none is supplied to the constructor. */
	public static final char	DEFAULT_SEPARATOR					= ',';
	
	public static final int		INITIAL_READ_SIZE					= 128;
	
	/**
	 * The default quote character to use if none is supplied to the
	 * constructor.
	 */
	public static final char	DEFAULT_QUOTE_CHARACTER				= '"';
	
	/**
	 * The default escape character to use if none is supplied to the
	 * constructor.
	 */
	public static final char	DEFAULT_ESCAPE_CHARACTER			= '\\';
	
	/**
	 * The default strict quote behavior to use if none is supplied to the
	 * constructor.
	 */
	public static final boolean	DEFAULT_STRICT_QUOTES				= false;
	
	/**
	 * The default leading whitespace behavior to use if none is supplied to the
	 * constructor.
	 */
	public static final boolean	DEFAULT_IGNORE_LEADING_WHITESPACE	= true;
	

	/**
	 * Constructs CSVParser using a comma for the separator.
	 */
	public CSVParser()
	{
		this(DEFAULT_SEPARATOR,DEFAULT_QUOTE_CHARACTER,DEFAULT_ESCAPE_CHARACTER);
	}
	

	/**
	 * Constructs CSVParser with supplied separator.
	 * 
	 * @param separator
	 *            the delimiter to use for separating entries.
	 */
	public CSVParser(char separator)
	{
		this(separator,DEFAULT_QUOTE_CHARACTER,DEFAULT_ESCAPE_CHARACTER);
	}
	

	/**
	 * Constructs CSVParser with supplied separator and quote char.
	 * 
	 * @param separator
	 *            the delimiter to use for separating entries
	 * @param quotechar
	 *            the character to use for quoted elements
	 */
	public CSVParser(char separator, char quotechar)
	{
		this(separator,quotechar,DEFAULT_ESCAPE_CHARACTER);
	}
	

	/**
	 * Constructs CSVReader with supplied separator and quote char.
	 * 
	 * @param separator
	 *            the delimiter to use for separating entries
	 * @param quotechar
	 *            the character to use for quoted elements
	 * @param escape
	 *            the character to use for escaping a separator or quote
	 */
	public CSVParser(char separator, char quotechar, char escape)
	{
		this(separator,quotechar,escape,DEFAULT_STRICT_QUOTES);
	}
	

	/**
	 * Constructs CSVReader with supplied separator and quote char. Allows
	 * setting the "strict quotes" flag
	 * 
	 * @param separator
	 *            the delimiter to use for separating entries
	 * @param quotechar
	 *            the character to use for quoted elements
	 * @param escape
	 *            the character to use for escaping a separator or quote
	 * @param strictQuotes
	 *            if true, characters outside the quotes are ignored
	 */
	public CSVParser(char separator, char quotechar, char escape, boolean strictQuotes)
	{
		this(separator,quotechar,escape,strictQuotes,DEFAULT_IGNORE_LEADING_WHITESPACE);
	}
	

	/**
	 * Constructs CSVReader with supplied separator and quote char. Allows
	 * setting the "strict quotes" and "ignore leading whitespace" flags
	 * 
	 * @param separator
	 *            the delimiter to use for separating entries
	 * @param quotechar
	 *            the character to use for quoted elements
	 * @param escape
	 *            the character to use for escaping a separator or quote
	 * @param strictQuotes
	 *            if true, characters outside the quotes are ignored
	 * @param ignoreLeadingWhiteSpace
	 *            if true, white space in front of a quote in a field is ignored
	 */
	public CSVParser(char separator, char quotechar, char escape, boolean strictQuotes,
			boolean ignoreLeadingWhiteSpace)
	{
		this.separator = separator;
		this.quotechar = quotechar;
		this.escape = escape;
		this.strictQuotes = strictQuotes;
		this.ignoreLeadingWhiteSpace = ignoreLeadingWhiteSpace;
	}
	

	/**
	 * 
	 * @return true if something was left over from last call(s)
	 */
	public boolean isPending()
	{
		return pending != null;
	}
	

	public String[] parseLineMulti(String nextLine) throws IOException
	{
		return parseLine(nextLine,true);
	}
	

	public String[] parseLine(String nextLine) throws IOException
	{
		return parseLine(nextLine,false);
	}
	

	/**
	 * Parses an incoming String and returns an array of elements.
	 * 
	 * @param nextLine
	 *            the string to parse
	 * @param multi
	 * @return the comma-tokenized list of elements, or null if nextLine is null
	 * @throws IOException
	 *             if bad things happen during the read
	 */
	private String[] parseLine(String nextLine, boolean multi) throws IOException
	{
		if(!multi && pending != null)
		{
			pending = null;
		}
		
		if(nextLine == null)
		{
			if(pending != null)
			{
				String s = pending;
				pending = null;
				return new String[]{s};
			}
			else
			{
				return null;
			}
		}
		
		List<String> tokensOnThisLine = new ArrayList<String>();
		StringBuilder sb = new StringBuilder(INITIAL_READ_SIZE);
		boolean inQuotes = false;
		if(pending != null)
		{
			sb.append(pending);
			pending = null;
			inQuotes = true;
		}
		int len = nextLine.length();
		for(int i = 0; i < len; i++)
		{			
			char c = nextLine.charAt(i);
			if(c == this.escape)
			{
				if(isNextCharacterEscapable(nextLine,inQuotes || inField,i))
				{
					sb.append(nextLine.charAt(i + 1));
					i++;
				}
			}
			else if(c == quotechar)
			{
				if(isNextCharacterEscapedQuote(nextLine,inQuotes || inField,i))
				{
					sb.append(nextLine.charAt(i + 1));
					i++;
				}
				else
				{
					inQuotes = !inQuotes;
					
					// the tricky case of an embedded quote in the middle:
					// a,bc"d"ef,g
					if(!strictQuotes)
					{
						if(i > 2 // not on the beginning of the line
								&& nextLine.charAt(i - 1) != this.separator // not
								// at
								// the
								// beginning
								// of
								// an
								// escape
								// sequence
								&& nextLine.length() > (i + 1)
								&& nextLine.charAt(i + 1) != this.separator // not
						// at
						// the
						// end
						// of
						// an
						// escape
						// sequence
						)
						{
							
							if(ignoreLeadingWhiteSpace && sb.length() > 0 && isAllWhiteSpace(sb))
							{
								sb = new StringBuilder(INITIAL_READ_SIZE); // discard
								// white
								// space
								// leading
								// up
								// to
								// quote
							}
							else
							{
								sb.append(c);
							}
							
						}
					}
				}
				inField = !inField;
			}
			else if(c == separator && !inQuotes)
			{
				tokensOnThisLine.add(sb.toString());
				sb = new StringBuilder(INITIAL_READ_SIZE); // start work on next
				// token
				inField = false;
			}
			else
			{
				if(!strictQuotes || inQuotes)
				{
					sb.append(c);
					inField = true;
				}
			}
		}
		// line is done - check status
		if(inQuotes)
		{
			if(multi)
			{
				// continuing a quoted section, re-append newline
				sb.append("\n");
				pending = sb.toString();
				sb = null; // this partial content is not to be added to field
				// list yet
			}
			else
			{
				throw new IOException("Un-terminated quoted field at end of CSV line");
			}
		}
		if(sb != null)
		{
			tokensOnThisLine.add(sb.toString());
		}
		return tokensOnThisLine.toArray(new String[tokensOnThisLine.size()]);		
	}
	

	/**
	 * precondition: the current character is a quote or an escape.
	 * 
	 * @param nextLine
	 *            the current line
	 * @param inQuotes
	 *            true if the current context is quoted
	 * @param i
	 *            current index in line
	 * @return true if the following character is a quote
	 */
	private boolean isNextCharacterEscapedQuote(String nextLine, boolean inQuotes, int i)
	{
		return inQuotes // we are in quotes, therefore there can be escaped
				// quotes in here.
				&& nextLine.length() > (i + 1) // there is indeed another
				// character to check.
				&& nextLine.charAt(i + 1) == quotechar;
	}
	

	/**
	 * precondition: the current character is an escape.
	 * 
	 * @param nextLine
	 *            the current line
	 * @param inQuotes
	 *            true if the current context is quoted
	 * @param i
	 *            current index in line
	 * @return true if the following character is a quote
	 */
	protected boolean isNextCharacterEscapable(String nextLine, boolean inQuotes, int i)
	{
		return inQuotes // we are in quotes, therefore there can be escaped
				// quotes in here.
				&& nextLine.length() > (i + 1) // there is indeed another
				// character to check.
				&& (nextLine.charAt(i + 1) == quotechar || nextLine.charAt(i + 1) == this.escape);
	}
	

	/**
	 * precondition: sb.length() > 0.
	 * 
	 * @param sb
	 *            A sequence of characters to examine
	 * @return true if every character in the sequence is whitespace
	 */
	protected boolean isAllWhiteSpace(CharSequence sb)
	{
		for(int i = 0; i < sb.length(); i++)
		{
			char c = sb.charAt(i);
			
			if(!Character.isWhitespace(c))
			{
				return false;
			}
		}
		return true;
	}
}
